버퍼를 구조체로 캐스팅해 읽기 — 구조체 안의 구조체 배열과 인덱스 접근
NOTE
연속된 바이트 버퍼(Raw Data Buffer, 수신 패킷, 공유메모리)를 구조체 포인터로 캐스팅하고, 그 안에 붙어 있는 구조체 배열을
tuple->member[i].field로 읽는 방법을 정리한다. 배열 길이를 누가 아는가, 패딩 때문에 오프셋이 어떻게 달라지는가가 핵심이다.
1. 어떤 상황인가
[ 바이트 버퍼 ] → (struct __tuple *) 로 캐스팅 → 필드 이름으로 접근
복사·파싱 없이 포인터 캐스팅 한 번으로 읽을 수 있어서 빠르다. 단, 버퍼의 바이트 배치가 구조체 선언과 정확히 같아야 한다.
2. 구조체 선언 — 반복 요소는 “배열 멤버”로 둔다
struct __test {
char str_val[5];
int length;
};
struct __tuple {
char str_val[5];
struct __test __struct[1]; /* 뒤에 같은 요소가 여러 개 이어 붙는다 — 마지막 멤버 */
} *tuple;
[i]로 접근하려면 멤버가 배열이어야 한다
struct __test __struct;(배열 아님)로 선언하면tuple->__struct[i]는 컴파일 에러다 — 단일 구조체에는 인덱스를 쓸 수 없다. 요소가 여러 개 붙는 구조라면__struct[1](또는 C99__struct[])처럼 배열 멤버로 선언한다. 이름도struct test/struct __test처럼 어긋나지 않게 맞춘다.마지막 멤버를
[1]/[]로 두는 것이 Struct Hack과 같은 원리다. 선언상 1개짜리지만 실제 버퍼에는 n개가 이어 붙어 있다고 약속하는 것이다.
3. 접근 방법
int i;
for (i = 0; i < n; i++) {
printf("%.5s %d\n",
tuple->__struct[i].str_val, /* 요소는 "값"이므로 . 으로 접근 */
tuple->__struct[i].length);
}| 표현 | 의미 |
|---|---|
tuple->str_val | 헤더 필드 (tuple이 포인터라 ->) |
tuple->__struct[i] | i번째 struct __test 값 (배열 요소) |
tuple->__struct[i].str_val | 그 요소의 필드 — 요소가 값이므로 . |
(tuple->__struct + i)->str_val | 같은 뜻. 포인터 산술로 쓴 형태 (a[i] ≡ *(a+i)) |
->는 포인터에,.는 구조체 값에 쓴다.tuple(포인터) →->,__struct[i](배열 요소=값) →..__struct[i]의 주소는기준 + i * sizeof(struct __test)로 계산된다 — 그래서 요소 크기(패딩 포함)가 버퍼와 일치해야 한다.
4. 개수(n)는 누가 아는가
컴파일러는 __struct[1]의 길이를 1로만 안다. 실제 개수는 데이터에서 얻어야 한다.
| 방법 | 설명 |
|---|---|
| 헤더에 개수 필드 | struct __tuple { int count; ... } 처럼 앞에 두고 i < tuple->count |
| 버퍼 전체 크기로 계산 | n = (size - offsetof(struct __tuple, __struct)) / sizeof(struct __test) (수신 함수가 돌려준 전체 바이트 수 size를 쓰는 경우) |
| 종료 요소 | 마지막 요소에 특수값(예: length == 0)을 두고 만날 때까지 순회 |
개수를 확인하지 않고 i를 돌리면 버퍼 밖을 읽는다(오버리드).
5. 패딩과 오프셋 — 크기를 눈대중으로 세지 않는다
x86-64 기준(char=1, int=4바이트·4정렬) 레이아웃:
| 구조체 | 필드 | 오프셋 | 비고 |
|---|---|---|---|
struct __test | str_val[5] | 0 | 5바이트 |
| (padding) | 5~7 | int를 4배수 주소에 맞추려고 3바이트 | |
length | 8 | ||
sizeof | 12 | 5+4=9가 아니라 12 | |
struct __tuple | str_val[5] | 0 | |
| (padding) | 5~7 | 요소(__test)가 4정렬이라 3바이트 | |
__struct[0] | 8 | i번째는 8 + 12*i | |
sizeof | 20 | (__struct[1] 포함) |
(Python ctypes로 같은 선언의 크기/오프셋을 계산해 확인한 값이다.)
- 요소 간격은
5 + 4 = 9가 아니라 12다. 버퍼를 만드는 쪽(다른 언어/장비/프로세스)이 9바이트 간격으로 채웠다면 구조체 캐스팅이 맞지 않는다 → 그 경우는memcpy로 필드를 하나씩 꺼내거나__attribute__((packed))를 쓴다 (packed는 정렬 위반 접근 비용·이식성 이슈가 있다). - 크기·오프셋은 하드코딩하지 말고
sizeof,offsetof(<stddef.h>)로 구한다. - 버퍼 시작 주소가 정렬되어 있지 않으면
int필드 접근이 느리거나 일부 CPU에서 오류가 난다 → 메모리 정렬과 aligned_alloc.
6. 문자열 필드 주의 — char str_val[5]
- 5바이트가 모두 데이터로 차 있으면
\0이 없다.printf("%s", ...)는 뒤의 패딩/다음 필드까지 읽어 나갈 수 있다. - 그래서 고정폭 필드는
printf("%.5s", p)처럼 최대 길이를 지정하거나, 버퍼에 복사한 뒤\0을 직접 붙인다. - 고정폭 문자열 필드를 구조체로 매핑할 때
char c[6]처럼 폭 +1바이트를 잡아 NUL 종료 문자 자리를 확보하는 관례도 있다.
7. 포인터 멤버와 헷갈리지 말 것
struct __bad {
char str_val[5];
struct __test *__struct; /* 포인터 멤버 */
};- 이 선언은 구조체 안에 요소가 들어있는 것이 아니라 “다른 곳의 주소”가 들어있다. 표기는 똑같이
p->__struct[i].str_val이지만 메모리 모델이 다르다. - 바이트 버퍼를 캐스팅해서 읽는 용도로는 쓸 수 없다 — 버퍼에는 요소 데이터가 이어 붙어 있을 뿐 주소값이 아니므로, 포인터 멤버는 버퍼의 일부 바이트를 주소로 잘못 해석해 크래시가 난다.
- 구분법: 요소 데이터가 같은 블록에 이어 붙음 → 배열 멤버(
[1]/[]), 별도 할당 → 포인터 멤버(해제도 따로).
8. 정리
| 질문 | 답 |
|---|---|
반복 요소를 [i]로 읽으려면? | 구조체의 마지막 배열 멤버로 선언 |
.과 -> 구분은? | 포인터면 ->, 배열 요소(값)면 . |
| 개수는? | 헤더 필드 / 전체 크기 계산 / 종료 요소로 직접 알아내야 함 |
| 요소 간격은? | sizeof(요소) (패딩 포함) — offsetof/sizeof로 계산 |
| 고정폭 문자열 출력은? | %.Ns 또는 복사 후 \0 부여 |
| 포인터 멤버로 대체 가능? | 불가 — 버퍼 캐스팅에서는 쓸 수 없음 |
관련 문서
- 가변 길이 구조체(Struct Hack)와 필러 필드 —
char data[1]의length + 가변 바이트구조 (OCTETS 예시 포함) - 메모리 정렬(Memory Alignment)과 aligned_alloc
- (CS) 포인터 - 핵심 개념 및 특징 정리